using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using MongoScienceDAL.Core.Exceptions;

namespace MongoScienceDAL.Core
{
    public class ScienceRepository : IScienceRepository
    {
        private readonly IDataSource _dataSource;
        private readonly MongoDatabase _database;
        private readonly ScienceCollection _collection;
        public string CollectionName { get; set; }

        public ScienceRepository(IDataSource dataSource, string collectionName) : this(dataSource)
        {
            _dataSource = dataSource;
            //check collection exists
            var collections = dataSource.QueryCollections().Where(x => x.Name == collectionName).ToList();
            if (collections.Count <= 0)
                throw new NoSuchCollectionException();

            _collection = collections.First();
            CollectionName = _collection.Name;
        }

        public ScienceRepository(IDataSource dataSource)
        {
            _dataSource = dataSource;
            _database = dataSource.Database;
        }

        public IList<ScienceEntry> Query(QueryCriteria criteria)
        {
            var query = MongoQueryBuilder.Process(criteria);

            //if we only want certain fields
            if(criteria.Selectors.Count > 0)
            {
                var fields = criteria.Selectors.Select(x => x.Key).ToList();
                fields.Add("_id");
                fields.Add("_col");
                return _database.GetCollection<ScienceEntry>(CollectionName).Find(query).SetFields(fields.ToArray()).ToList();
            }

            //if all fields then just query
            var results = _database.GetCollection<ScienceEntry>(CollectionName).Find(query);
            return results.ToList();
        }

        public IPaginatedList<ScienceEntry> QueryPaged(QueryCriteria criteria, int pageIndex, int pageSize)
        {
            var query = MongoQueryBuilder.Process(criteria);

            //first get total count
            var totalCount = (int)Count(criteria);

            //if we only want certain fields1
            if (criteria.Selectors.Count > 0)
            {
                var fields = criteria.Selectors.Select(x => x.Key).ToList();
                fields.Add("_id");
                fields.Add("_col");
                return new PaginatedList<ScienceEntry>(_database.GetCollection<ScienceEntry>(CollectionName)
                            .Find(query)
                            .SetFields(fields.ToArray())
                            .SetSkip(pageIndex * pageSize)
                            .SetLimit(pageSize), pageIndex, pageSize, totalCount);
            }

            //if all fields then just query
            var results = _database.GetCollection<ScienceEntry>(CollectionName)
                        .Find(query)
                        .SetSkip(pageIndex * pageSize)
                        .SetLimit(pageSize);
            return new PaginatedList<ScienceEntry>(results, pageIndex, pageSize, totalCount);
        } 

        public long Count(QueryCriteria criteria)
        {
            var query = MongoQueryBuilder.Process(criteria);
            return _database.GetCollection<ScienceEntry>(CollectionName).Count(query);
        }

        public ObjectId Create(IDictionary<string, object> data)
        {
            //check if data is null
            if (data == null)
                data = new Dictionary<string, object>();

            var scienceEntry = new ScienceEntry {Collection = _collection, Data = data};
            var result = _database.GetCollection<ScienceEntry>(CollectionName).Save(scienceEntry);
            return result.Response["upserted"].AsObjectId;
        }

        public ScienceEntry Retrieve(ObjectId id)
        {
            return _database.GetCollection<ScienceEntry>(CollectionName).FindOne(MongoDB.Driver.Builders.Query.EQ("_id", id));
        }

        public void Update(ScienceEntry entryToUpdate)
        {
            _database.GetCollection<ScienceEntry>(CollectionName).Save(entryToUpdate);
        }

        public void Delete(ObjectId id)
        {
            _database.GetCollection<ScienceEntry>(CollectionName).Remove(new QueryDocument("_id", id));
        }

        public object RetrieveField(ObjectId id, string key)
        {
            return Query(new QueryCriteria().AddObjectIdEquals(id).AddSelector(key)).First().Data[key];
        }

        public bool HasField(ObjectId id, string key)
        {
            //if col or id always return true
            if (key == "_id" || key == "_col")
                return true;

            //query for the data and return if the data field of the science entry has the required key
            return Query(new QueryCriteria().AddObjectIdEquals(id).AddSelector(key)).First().Data.ContainsKey(key);
        }

        public void UpdateField(ObjectId id, string key, object value)
        {
            //we have no choice but to do custom serialisation, in the future this should be merged with ScienceEntrySerialiser

            //Get science types for collection
            var types = _dataSource.QueryCollectionTypes(CollectionName).ToList();

            //check key exists
            if (types.All(x => x.Key != key))
                throw new ArgumentException("No such key in collection - " + key, "key");

            //get type
            var type = types.First(x => x.Key == key);

            //prepare writer
            var dataDocument = new BsonDocument();
            var bsonWriter = BsonWriter.Create(dataDocument);
            var scienceEntryWriter = new ScienceEntryBsonWriter(bsonWriter);

            bsonWriter.WriteStartDocument();

            scienceEntryWriter.Write(type.Type, key, value);

            bsonWriter.WriteEndDocument();

            //build query
            var query = new QueryDocument("_id", id);

            //prepare update
            var update = new UpdateDocument { {"$set", dataDocument} };

            //update while treating the collection as bsondocument type rather than scienceentry.
            _database.GetCollection(CollectionName).Update(query, update);
        }
    }
}